﻿#region Copyright (c) 2013-03, Olaf Kober <amarok.projects@gmail.com>
//================================================================================
//	Permission is hereby granted, free of charge, to any person obtaining a copy
//	of this software and associated documentation files (the "Software"), to deal
//	in the Software without restriction, including without limitation the rights
//	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//	copies of the Software, and to permit persons to whom the Software is
//	furnished to do so, subject to the following conditions:
//
//	The above copyright notice and this permission notice shall be included in
//	all copies or substantial portions of the Software.
//
//	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//	THE SOFTWARE.
//================================================================================
#endregion

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Amarok.Agents.Resources;


namespace Amarok.Agents
{
	/// <summary>
	/// This type implements a subsystem.
	/// 
	/// Subsystems are primarily used to implement startup and loading logic by spawning agents and optionally 
	/// loading other subsystems, which again can spawn agents and load other subsystems.
	/// </summary>
	public abstract class Subsystem : CompletableObject,
		ISubsystem
	{
		// static data
		private static ISubsystemFactory sFactory = new DefaultSubsystemFactory();

		// data
		private readonly RuntimeEnvironment mEnvironment;
		private readonly ConcurrentStack<ICompletable> mCompletables = new ConcurrentStack<ICompletable>();


		#region ++ Public Static Interface ++

		/// <summary>
		/// Gets a reference to the subsystem factory.
		/// </summary>
		public static ISubsystemFactory Factory
		{
			get
			{
				return sFactory;
			}
		}


		/// <summary>
		/// Replaces the default subsystem factory by the factory supplied to the method.
		/// </summary>
		/// 
		/// <param name="factory">
		/// The new subsystem factory to use.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		public static void SetFactory(ISubsystemFactory factory)
		{
			Verify.NotNull(factory, "factory");
			sFactory = factory;
		}

		#endregion

		#region ++ ISubsystem Interface ++

		/// <summary>
		/// Loads the subsystem and all agents and subsystems in it.
		/// </summary>
		/// 
		/// <exception cref="SubsystemLoadException">
		/// An instance of the specified subsystem type could not be loaded. Refer to the inner exception for more 
		/// information.</exception>
		void ISubsystem.Load()
		{
			try
			{
				OnLoad();
			}
			catch (SubsystemLoadException)
			{
				throw;
			}
			catch (Exception exception)
			{
				throw new SubsystemLoadException(
					SubsystemResources.SubsystemLoadFailure,
					exception);
			}
		}

		#endregion

		#region ## Protected Interface ##

		/// <summary>
		/// Gets a reference to the subsystem's agent environment. The agent environment is used to give agents 
		/// access to the services of the runtime environment. One agent environment instance is shared between 
		/// multiple agents to provide the same services to all agents.
		/// </summary>
		protected RuntimeEnvironment Environment
		{
			get
			{
				return mEnvironment;
			}
		}


		/// <summary>
		/// Initializes a new instance.
		/// </summary>
		/// 
		/// <param name="environment">
		/// The runtime environment for this subsystem.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		protected Subsystem(RuntimeEnvironment environment)
		{
			Verify.NotNull(environment, "environment");
			mEnvironment = environment;
		}

		#endregion

		#region ## Protected Interface (LoadSubsystem) ##

		/// <summary>
		/// Loads the subsystem of the specified subsystem type.
		/// </summary>
		/// 
		/// <param name="subsystemType">
		/// The type of subsystem to load. This must be a sub-class of <see cref="Subsystem"/>.</param>
		/// <param name="environment">
		/// The runtime environment to be supplied to the subsystem.</param>
		/// 
		/// <returns>
		/// A reference to the newly loaded subsystem.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// A value was passed to a method that did not accept it as a valid argument, because 
		/// the value representing a type must be a sub-class of <see cref="Subsystem"/>.</exception>
		/// <exception cref="SubsystemLoadException">
		/// An instance of the specified subsystem type cannot be instantiated, because the subsystem type does not 
		/// provide a subsystem-specific constructor that accepts a runtime environment. Provide a constructor with 
		/// following signature: Subsystem(RuntimeEnvironment).</exception>
		/// <exception cref="SubsystemLoadException">
		/// An instance of the specified subsystem type could not be loaded. Refer to the inner exception for more 
		/// information.</exception>
		protected ISubsystem LoadSubsystem(Type subsystemType, RuntimeEnvironment environment)
		{
			return _RegisterSubsystem(
				Subsystem.Factory.LoadSubsystem(
					subsystemType,
					environment
			));
		}

		/// <summary>
		/// Loads the subsystem of the specified subsystem type.
		/// </summary>
		/// 
		/// <param name="subsystemType">
		/// The type of subsystem to load. This must be a sub-class of <see cref="Subsystem"/>.</param>
		/// 
		/// <returns>
		/// A reference to the newly loaded subsystem.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// A value was passed to a method that did not accept it as a valid argument, because 
		/// the value representing a type must be a sub-class of <see cref="Subsystem"/>.</exception>
		/// <exception cref="SubsystemLoadException">
		/// An instance of the specified subsystem type cannot be instantiated, because the subsystem type does not 
		/// provide a subsystem-specific constructor that accepts a runtime environment. Provide a constructor with 
		/// following signature: Subsystem(RuntimeEnvironment).</exception>
		/// <exception cref="SubsystemLoadException">
		/// An instance of the specified subsystem type could not be loaded. Refer to the inner exception for more 
		/// information.</exception>
		protected ISubsystem LoadSubsystem(Type subsystemType)
		{
			return _RegisterSubsystem(
				Subsystem.Factory.LoadSubsystem(
					subsystemType,
					mEnvironment
			));
		}


		/// <summary>
		/// Loads the subsystem of the specified subsystem type.
		/// </summary>
		/// 
		/// <typeparam name="TSubsystem">
		/// The type of subsystem to load. This must be a sub-class of <see cref="Subsystem"/>.</typeparam>
		/// 
		/// <param name="environment">
		/// The runtime environment to be supplied to the subsystem.</param>
		/// 
		/// <returns>
		/// A reference to the newly loaded subsystem.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="SubsystemLoadException">
		/// An instance of the specified subsystem type cannot be instantiated, because the subsystem type does not 
		/// provide a subsystem-specific constructor that accepts a runtime environment. Provide a constructor with 
		/// following signature: Subsystem(RuntimeEnvironment).</exception>
		/// <exception cref="SubsystemLoadException">
		/// An instance of the specified subsystem type could not be loaded. Refer to the inner exception for more 
		/// information.</exception>
		protected ISubsystem LoadSubsystem<TSubsystem>(RuntimeEnvironment environment)
			where TSubsystem : Subsystem
		{
			return _RegisterSubsystem(
				Subsystem.Factory.LoadSubsystem<TSubsystem>(
					environment
			));
		}

		/// <summary>
		/// Loads the subsystem of the specified subsystem type.
		/// </summary>
		/// 
		/// <typeparam name="TSubsystem">
		/// The type of subsystem to load. This must be a sub-class of <see cref="Subsystem"/>.</typeparam>
		/// 
		/// <returns>
		/// A reference to the newly loaded subsystem.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="SubsystemLoadException">
		/// An instance of the specified subsystem type cannot be instantiated, because the subsystem type does not 
		/// provide a subsystem-specific constructor that accepts a runtime environment. Provide a constructor with 
		/// following signature: Subsystem(RuntimeEnvironment).</exception>
		/// <exception cref="SubsystemLoadException">
		/// An instance of the specified subsystem type could not be loaded. Refer to the inner exception for more 
		/// information.</exception>
		protected ISubsystem LoadSubsystem<TSubsystem>()
			where TSubsystem : Subsystem
		{
			return _RegisterSubsystem(
				Subsystem.Factory.LoadSubsystem<TSubsystem>(
					mEnvironment
			));
		}


		/// <summary>
		/// Loads the subsystem of the specified subsystem type.
		/// </summary>
		/// 
		/// <typeparam name="TSubsystem">
		/// The type of subsystem to load. This must be a sub-class of <see cref="Subsystem"/>.</typeparam>
		/// <typeparam name="TEnvironment">
		/// The type of runtime environment to supply. This must be a sub-class of <see cref="RuntimeEnvironment"/>.</typeparam>
		/// 
		/// <param name="environment">
		/// The runtime environment to be supplied to the subsystem.</param>
		/// 
		/// <returns>
		/// A reference to the newly loaded subsystem.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="SubsystemLoadException">
		/// An instance of the specified subsystem type cannot be instantiated, because the subsystem type does not 
		/// provide a subsystem-specific constructor that accepts a runtime environment. Provide a constructor with 
		/// following signature: Subsystem(RuntimeEnvironment).</exception>
		/// <exception cref="SubsystemLoadException">
		/// An instance of the specified subsystem type could not be loaded. Refer to the inner exception for more 
		/// information.</exception>
		protected ISubsystem LoadSubsystem<TSubsystem, TEnvironment>(TEnvironment environment)
			where TSubsystem : Subsystem
			where TEnvironment : RuntimeEnvironment
		{
			return _RegisterSubsystem(
				Subsystem.Factory.LoadSubsystem<TSubsystem, TEnvironment>(
					environment
			));
		}

		#endregion

		#region ## Protected Interface (SpawnAgent) ##

		/// <summary>
		/// Spawns a new instance of the specified agent type.
		/// </summary>
		/// 
		/// <param name="agentType">
		/// The type of agent to spawn. This must be a sub-class of <see cref="Agent"/>.</param>
		/// <param name="environment">
		/// The agent environment to be supplied to the new agent instance.</param>
		/// <param name="options">
		/// The agent options to be supplied to the new agent instance.</param>
		/// 
		/// <returns>
		/// A reference to the newly spawned agent instance.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// A value was passed to a method that did not accept it as a valid argument, because 
		/// the value representing a type must be a sub-class of <see cref="Agent"/>.</exception>
		/// <exception cref="AgentSpawnException">
		/// An instance of the specified agent type cannot be instantiated, because the agent type does not 
		/// provide an agent-specific constructor that accepts agent environment and agent options. Provide 
		/// a constructor with following signature: Agent(AgentEnvironment, AgentOptions).</exception>
		/// <exception cref="AgentSpawnException">
		/// An instance of the specified agent type could not be spawned. Refer to the inner exception 
		/// for more information.</exception>
		protected IAgent SpawnAgent(Type agentType, AgentEnvironment environment, AgentOptions options)
		{
			return _RegisterAgent(
				Agent.Factory.SpawnAgent(
					agentType,
					environment,
					options
			));
		}

		/// <summary>
		/// Spawns a new instance of the specified agent type.
		/// </summary>
		/// 
		/// <param name="agentType">
		/// The type of agent to spawn. This must be a sub-class of <see cref="Agent"/>.</param>
		/// <param name="options">
		/// The agent options to be supplied to the new agent instance.</param>
		/// 
		/// <returns>
		/// A reference to the newly spawned agent instance.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// A value was passed to a method that did not accept it as a valid argument, because 
		/// the value representing a type must be a sub-class of <see cref="Agent"/>.</exception>
		/// <exception cref="AgentSpawnException">
		/// An instance of the specified agent type cannot be instantiated, because the agent type does not 
		/// provide an agent-specific constructor that accepts agent environment and agent options. Provide 
		/// a constructor with following signature: Agent(AgentEnvironment, AgentOptions).</exception>
		/// <exception cref="AgentSpawnException">
		/// An instance of the specified agent type could not be spawned. Refer to the inner exception 
		/// for more information.</exception>
		protected IAgent SpawnAgent(Type agentType, AgentOptions options)
		{
			return _RegisterAgent(
				Agent.Factory.SpawnAgent(
					agentType,
					mEnvironment,
					options
			));
		}

		/// <summary>
		/// Spawns a new instance of the specified agent type.
		/// </summary>
		/// 
		/// <param name="agentType">
		/// The type of agent to spawn. This must be a sub-class of <see cref="Agent"/>.</param>
		/// 
		/// <returns>
		/// A reference to the newly spawned agent instance.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// A value was passed to a method that did not accept it as a valid argument, because 
		/// the value representing a type must be a sub-class of <see cref="Agent"/>.</exception>
		/// <exception cref="AgentSpawnException">
		/// An instance of the specified agent type cannot be instantiated, because the agent type does not 
		/// provide an agent-specific constructor that accepts agent environment and agent options. Provide 
		/// a constructor with following signature: Agent(AgentEnvironment, AgentOptions).</exception>
		/// <exception cref="AgentSpawnException">
		/// An instance of the specified agent type could not be spawned. Refer to the inner exception 
		/// for more information.</exception>
		protected IAgent SpawnAgent(Type agentType)
		{
			return _RegisterAgent(
				Agent.Factory.SpawnAgent(
					agentType,
					mEnvironment
			));
		}


		/// <summary>
		/// Spawns a new instance of the specified agent type.
		/// </summary>
		/// 
		/// <typeparam name="TAgent">
		/// The type of agent to spawn. This must be a sub-class of <see cref="Agent"/>.</typeparam>
		/// 
		/// <param name="environment">
		/// The agent environment to be supplied to the new agent instance.</param>
		/// <param name="options">
		/// The agent options to be supplied to the new agent instance.</param>
		/// 
		/// <returns>
		/// A reference to the newly spawned agent instance.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="AgentSpawnException">
		/// An instance of the specified agent type cannot be instantiated, because the agent type does not 
		/// provide an agent-specific constructor that accepts agent environment and agent options. Provide 
		/// a constructor with following signature: Agent(AgentEnvironment, AgentOptions).</exception>
		/// <exception cref="AgentSpawnException">
		/// An instance of the specified agent type could not be spawned. Refer to the inner exception 
		/// for more information.</exception>
		protected IAgent SpawnAgent<TAgent>(AgentEnvironment environment, AgentOptions options)
			where TAgent : Agent
		{
			return _RegisterAgent(
				Agent.Factory.SpawnAgent<TAgent>(
					environment,
					options
			));
		}

		/// <summary>
		/// Spawns a new instance of the specified agent type.
		/// </summary>
		/// 
		/// <typeparam name="TAgent">
		/// The type of agent to spawn. This must be a sub-class of <see cref="Agent"/>.</typeparam>
		/// 
		/// <param name="options">
		/// The agent options to be supplied to the new agent instance.</param>
		/// 
		/// <returns>
		/// A reference to the newly spawned agent instance.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="AgentSpawnException">
		/// An instance of the specified agent type cannot be instantiated, because the agent type does not 
		/// provide an agent-specific constructor that accepts agent environment and agent options. Provide 
		/// a constructor with following signature: Agent(AgentEnvironment, AgentOptions).</exception>
		/// <exception cref="AgentSpawnException">
		/// An instance of the specified agent type could not be spawned. Refer to the inner exception 
		/// for more information.</exception>
		protected IAgent SpawnAgent<TAgent>(AgentOptions options)
			where TAgent : Agent
		{
			return _RegisterAgent(
				Agent.Factory.SpawnAgent<TAgent>(
					mEnvironment,
					options
			));
		}

		/// <summary>
		/// Spawns a new instance of the specified agent type.
		/// </summary>
		/// 
		/// <typeparam name="TAgent">
		/// The type of agent to spawn. This must be a sub-class of <see cref="Agent"/>.</typeparam>
		/// 
		/// <returns>
		/// A reference to the newly spawned agent instance.</returns>
		/// 
		/// <exception cref="AgentSpawnException">
		/// An instance of the specified agent type cannot be instantiated, because the agent type does not 
		/// provide an agent-specific constructor that accepts agent environment and agent options. Provide 
		/// a constructor with following signature: Agent(AgentEnvironment, AgentOptions).</exception>
		/// <exception cref="AgentSpawnException">
		/// An instance of the specified agent type could not be spawned. Refer to the inner exception 
		/// for more information.</exception>
		protected IAgent SpawnAgent<TAgent>()
			where TAgent : Agent
		{
			return _RegisterAgent(
				Agent.Factory.SpawnAgent<TAgent>(
					mEnvironment
			));
		}


		/// <summary>
		/// Spawns a new instance of the specified agent type.
		/// </summary>
		/// 
		/// <typeparam name="TAgent">
		/// The type of agent to spawn. This must be a sub-class of <see cref="Agent"/>.</typeparam>
		/// <typeparam name="TEnvironment">
		/// The agent environment to be supplied to the new agent instance.</typeparam>
		/// <typeparam name="TOptions">
		/// The agent options to be supplied to the new agent instance.</typeparam>
		/// 
		/// <param name="environment">
		/// The agent environment to be supplied to the new agent instance.</param>
		/// <param name="options">
		/// The agent options to be supplied to the new agent instance.</param>
		/// 
		/// <returns>
		/// A reference to the newly spawned agent instance.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="AgentSpawnException">
		/// An instance of the specified agent type cannot be instantiated, because the agent type does not 
		/// provide an agent-specific constructor that accepts agent environment and agent options. Provide 
		/// a constructor with following signature: Agent(AgentEnvironment, AgentOptions).</exception>
		/// <exception cref="AgentSpawnException">
		/// An instance of the specified agent type could not be spawned. Refer to the inner exception 
		/// for more information.</exception>
		protected IAgent SpawnAgent<TAgent, TEnvironment, TOptions>(TEnvironment environment, TOptions options)
			where TAgent : Agent<TEnvironment, TOptions>
			where TEnvironment : AgentEnvironment
			where TOptions : AgentOptions
		{
			return _RegisterAgent(
				Agent.Factory.SpawnAgent<TAgent, TEnvironment, TOptions>(
					environment,
					options
			));
		}

		#endregion

		#region ## Overridable Methods ##

		/// <summary>
		/// An overrideable method that allows derived types to implement custom startup and loading logic. The 
		/// method is called once when the subsystem is loaded. Derived types should spawn agents and/or load
		/// other subsystems.
		/// </summary>
		protected virtual void OnLoad()
		{
			// default implementation
		}

		/// <summary>
		/// An overrideable method that allows derived types to implement custom cleanup logic. The cleanup
		/// method is called at last after the agent run to completion.
		/// 
		/// This method is guaranteed to be called once.
		/// </summary>
		protected virtual void OnCleanup()
		{
			// default implementation
		}

		#endregion

		#region Implementation

		/// <summary>
		/// This method supports the agent infrastructure and is not intended to be used directly.
		/// </summary>
		protected override Task OnComplete(Boolean discardAvailableMessages)
		{
			// get temporary copy
			var completables = mCompletables.ToArray();
			mCompletables.Clear();

			if (discardAvailableMessages)
			{
				// complete in parallel
				foreach (var subscription in completables)
					subscription.Complete(true);

				var completionTasks = completables.Select(
					item => item.Completion);

				return TaskHelper.WhenAll(completionTasks);
			}
			else
			{
				// complete sequentially in reverse order
				return Task.Factory.StartNew(() =>
					{
						foreach (var subscription in completables.Reverse<ICompletable>())
						{
							subscription.Complete(false);
							subscription.Completion.WaitQuietly();
						}
					})
					.IgnoreExceptions();
			}
		}

		/// <summary>
		/// This method supports the agent infrastructure and is not intended to be used directly.
		/// </summary>
		protected override void OnCompleted()
		{
			try
			{
				// call sub-class
				OnCleanup();
			}
			catch (Exception exception)
			{
				#region (tracing)
				{
					Trace.WriteLine(String.Format(CultureInfo.InvariantCulture,
						"*** WARNING: UNHANDLED EXCEPTION in Subsystem.OnCleanup():" +
						"\r\n  ObjectId: {0}" +
						"\r\n\r\n{1}",
						base.ObjectId,
						Helper.FormatException(exception, CultureInfo.InvariantCulture)
					));
				}
				#endregion
			}
		}

		private IAgent _RegisterAgent(IAgent agent)
		{
			mCompletables.Push(agent);
			return agent;
		}

		private ISubsystem _RegisterSubsystem(ISubsystem subsystem)
		{
			mCompletables.Push(subsystem);
			return subsystem;
		}

		#endregion

	}
}
